using System;
using System.Diagnostics;
using WSKnowledge.WSHelpers.DesignByContract;
using NHibernate;

namespace WSKnowledge.WSHelpers.NHibernate
{
	public interface IUnitOfWork : IDisposable
	{
		//ISession Session { get; }
		bool IsOpen { get; }
		bool IsInTransaction { get; }

		void StartUp();
		void BeginTransaction();
		void Commit();
		void Commit(bool keepAlive);
		void Rollback();
		void Attach(object entity);
		ISession GetSession();
	}

	/// <summary>
	/// This is to create a unit of work corresponding to provided ISessionFactory.
	/// To finalize that unit of work you should call either Commit() or Rollback().
	/// To Restart this unit again, you should call StartUp(), but only when it has no active session.
	/// When you call Persist or Delete functions for the first time, BeginTransaction will be called.
	/// Otherwise, the unit of work won't have a transaction, read-only mode.
	/// </summary>
	[Serializable]
	public class UnitOfWork : IUnitOfWork
	{
		private const string ERRORMESSAGE_GENERAL_INVALID_STATE = "Invalid action according to unit's current state!";

		private const string ERRORMESSAGE_STARTUP_INVALID_STATE =
			ERRORMESSAGE_GENERAL_INVALID_STATE + " Can not start up a session while unit it is not active!";

		private const string ERRORMESSAGE_BEGINTRANSACTION_INVALID_STATE =
			ERRORMESSAGE_GENERAL_INVALID_STATE + " Can not begin a transaction while unit already in transaction!";

		private const string ERRORMESSAGE_COMMIT_ROLLBACK_INVALID_STATE =
			ERRORMESSAGE_GENERAL_INVALID_STATE + " Can not commit or rollback unit while it is not open!";

		private const string ERRORMESSAGE_DISPOSE_WITH_ACTIVE_TRANSACTION =
			"Dispose roll back the unit because it has an active transaction. Unit should be committed or rolled back before disposing.";

		private const string ERRORMESSAGE_FAILED_TO_COMMIT
			= "Failed to commit unit of work. Active transaction has been rolled back and session has been forced to closed.";

		private const string ERRORMESSAGE_INCONSISTENT_STATE = "Unit's current state is not consistent!";

		private ISession _session;
		private ITransaction _transaction;
		private readonly ISessionFactory _sessionFactory;

		#region UnitOfWork Properties

		public bool IsOpen
		{
			get { return _session != null && _session.IsOpen; }
		}

		public bool IsInTransaction
		{
			get { return _transaction != null && _transaction.IsActive; }
		}
		#endregion

		#region UnitOfWork Constuctors

		/// <summary>
		/// Initializes a new unit of work but does not start a session before receiving the first call.
		/// 
		/// Exceptions:
		///		AssertionException in case of null session factory.
		/// </summary>
		/// <param name="factory"></param>
		public UnitOfWork(ISessionFactory factory)
		{
			Check.Require(factory != null, "NHibernate session factory is required!");
			_sessionFactory = factory;
		}

		#endregion

		#region UnitOfWork Methods

		/// <summary>
		/// Begins (starts/restarts) a unit of work with a fresh session.
		/// UnitOfWorkState must not be Open or InTransaction. Otherwise, an UnitOfWorkInvalidStateException will be thrown.
		/// 
		/// Exceptions:
		///		AssertionException, if unit of work has insuitable state for the requested operation.
		/// </summary>
		public void StartUp()
		{
			if(IsOpen)
				throw new UnitOfWorkException(ERRORMESSAGE_STARTUP_INVALID_STATE);

			_session = _sessionFactory.OpenSession();
		}

		/// <summary>
		/// Returns current opened session.
		/// Remark: It doesn't open a new one this should be done when first creating this unit of work,
		/// or by means of StartUp() if the unit of work is closed, previously committed or rolled back.
		/// 
		/// Exceptions:
		///		UnitOfWorkInvalidStateException, if unit of work is not open (IsOpen is false).
		/// </summary>
		/// <returns>currently active or newly created one.</returns>
		public ISession GetSession()
		{
			if(!IsOpen) StartUp();
			return _session;
		}

		/// <summary>
		/// Begins (starts/restarts) a fresh transaction.
		/// UnitOfWorkState must not be InTransaction. Otherwise, an UnitOfWorkInvalidStateException will be thrown.
		/// 
		/// Exceptions:
		///		UnitOfWorkInvalidStateException, if unit of work is active.
		/// </summary>
		public void BeginTransaction()
		{
			if(IsInTransaction)
				throw new UnitOfWorkException(ERRORMESSAGE_BEGINTRANSACTION_INVALID_STATE);

			_transaction = GetSession().BeginTransaction();
		}

		/// <summary>
		/// Commits the active transaction, if any, flushes and closes current open session.
		///
		/// Exceptions:
		///		UnitOfWorkInvalidStateException, if unit of work is not active (IsOpen is false)
		/// </summary>
		public void Commit()
		{
			Commit(false);
		}

		/// <summary>
		/// Commits the active transaction, if any, flushes and closes current open session.
		///
		/// Exceptions:
		///		UnitOfWorkInvalidStateException, if unit of work is not active (IsOpen is false)
		/// </summary>
		/// <param name="keepAlive">
		/// Use true to keep session alive.
		/// </param>
		public void Commit(bool keepAlive)
		{
			if (!IsOpen)
				throw new UnitOfWorkException(ERRORMESSAGE_COMMIT_ROLLBACK_INVALID_STATE);

			// flush, commit and close session, but you should not make _session or _transaction = null
			// otherwise, WasCommitted & WasRolledBack properties will be always false.
			try
			{
				// flush active session and commit transaction, if any
				if (IsInTransaction)
					_transaction.Commit();
				else
					_session.Flush();

				if (keepAlive)
					if (_session == null || !_session.IsOpen)
						throw new UnitOfWorkException(ERRORMESSAGE_INCONSISTENT_STATE);
					else
						new object(); // simply, do nothing !! didn't have another idea to express it :(
				else
					_session.Close();
			}
			catch (Exception ex)
			{
				if (IsOpen) Rollback();
				throw new UnitOfWorkException(ERRORMESSAGE_FAILED_TO_COMMIT, ex);
			}
		}

		/// <summary>
		/// Rolls back active tranaction(if any) and may close session (without flushing as recommended by NHibernate).
		/// </summary>
		public void Rollback()
		{
			if (!IsOpen)
				throw new UnitOfWorkException(ERRORMESSAGE_COMMIT_ROLLBACK_INVALID_STATE);

			// rollback and close session, but you should not make _session & _transaction = null
			// otherwise, WasCommitted & WasRolledBack will be always false.
			if (IsInTransaction) _transaction.Rollback();

			// close without flushing (as recommended by NHibernate)
			_session.Close();
		}

		public void Attach(object entity)
		{
			GetSession().Lock(entity, LockMode.None);
		}

		#endregion

		#region IUnitOfWork Members

		public void Dispose()
		{
			Trace.WriteLine("Calling UnitOfWork.Dispose()");
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion

		/// <summary>
		/// On intended dispose of unit of work, unit will be committed. But, on unintended dispose of an active unit of work
		/// an exception will be thrown to indecate inapproprate use of unit of work
		/// </summary>
		/// <param name="disposing"></param>
		private void Dispose(bool disposing)
		{
			Trace.WriteLine("Call UnitOfWork.Dispose(" + disposing + ")");
			if (disposing)
			{
				if (IsOpen)
				{
					Trace.WriteLine("Dispose will commit the unit, first.");
					Commit();
				}
			}
			else
			{
				if (IsInTransaction)
				{
					Trace.WriteLine("Unit was not committed by user! Dispose will rollback the unit and throw an exception");
					Rollback();
					throw new UnitOfWorkException(ERRORMESSAGE_DISPOSE_WITH_ACTIVE_TRANSACTION);
				}
				if (IsOpen)
				{
					Trace.WriteLine("Dispose will close the unit session");
					_session.Close();
				}
			}
		}
	}

	/// <summary>
	/// Thrown when an error occured during performing an action.
	/// Usually, This will have an inner exception to indicate the main reason of exception
	/// </summary>
	public class UnitOfWorkException : Exception
	{
		public UnitOfWorkException(string message)
			: base(message)
		{
		}

		public UnitOfWorkException(string message, Exception inner)
			: base(message, inner)
		{
		}
	}
}